# -*- coding:UTF-8 -*-

import hashlib
import json
from time import time
from typing import Any, Dict, List, Optional
from urllib.parse import urlparse
import requests


class Blockchain:

    def __init__(self):
        self.current_transactions = []  # 待挖块的交易列表
        self.chain = []  # 完整链条
        self.nodes = set()  # 全网节点

        self.new_block(previous_hash='1', proof=100)  # 创建创世块

    @staticmethod
    def hash(block: Dict[str, Any]) -> str:
        """
        生成块的 SHA-256 hash值
        :param block: 区块
        """
        block_string = json.dumps(block, sort_keys=True).encode()  # 统一排序防止因字段顺序问题导致结果不一致

        return hashlib.sha256(block_string).hexdigest()

    def new_block(self, proof: int, previous_hash: Optional[str]) -> Dict[str, Any]:
        """
        往区块链中添加新的区块
        :param proof: 工作量证明
        :param previous_hash: 前一个区块hash值
        :return: 添加的区块信息
        """
        block = {
            'index': len(self.chain) + 1,  # 索引
            'timestamp': time(),  # 时间戳
            'transactions': self.current_transactions,  # 交易列表
            'proof': proof,  # 工作量证明
            'previous_hash': previous_hash or self.hash(self.chain[-1])  # 前一个区块hash值
        }

        self.current_transactions = []  # 清空交易列表，便于下个区块的交易加入
        self.chain.append(block)

        return block

    @property
    def last_block(self) -> Dict[str, Any]:
        """
        取得最后一个区块，作为属性
        :return: 区块信息
        """
        return self.chain[-1]

    def new_transaction(self, sender: str, recipient: str, amount: int) -> int:
        """
        生成新交易信息，信息将加入到下一个待挖的区块中
        :param sender: 发送方地址
        :param recipient: 接收方地址
        :param amount: 金额
        :return: 新增交易的索引
        """
        self.current_transactions.append({
            'sender': sender,
            'recipient': recipient,
            'amount': amount,
        })

        return self.last_block['index'] + 1

    @staticmethod
    def valid_proof(last_proof: int, proof: int) -> bool:
        """
        判断hash(last_proof proof)前4位是否都为0
        :param last_proof: 上一个块的证明
        :param proof: 当前的证明
        :return:
        """
        guess = f'{last_proof}{proof}'.encode()
        guess_hash = hashlib.sha256(guess).hexdigest()

        return guess_hash[:4] == "0000"  # 要增加计算难度只需要增加为0的个数

    def proof_of_work(self, last_proof: int) -> int:
        """
        简单的工作量证明:
         - 查找一个 p' 使得 hash(pp') 以4个0开头
         - p 是上一个块的证明,  p' 是当前的证明
        :param last_proof: 上一个块的证明
        :return: 计算所得的当前的工作证明值
        """
        proof = 0

        while self.valid_proof(last_proof, proof) is False:
            proof += 1

        return proof

    def register_node(self, address: str) -> None:
        """
        注册一个节点
        :param address: 形如'http://192.168.0.5:5000'的节点地址
        :return:
        """
        parsed_url = urlparse(address)
        self.nodes.add(parsed_url.netloc)

    def valid_chain(self, chain: List[Dict[str, Any]]) -> bool:
        """
        验证区块链是否是正确的
        :param chain: 区块链
        :return:
        """
        last_block = chain[0]
        current_index = 1

        while current_index < len(chain):
            block = chain[current_index]
            print(f'{last_block}')
            print(f'{block}')
            print("\n-----------\n")
            # 检测hash链是否正确
            if block['previous_hash'] != self.hash(last_block):
                return False

            # 检测工作量链是否正确
            if not self.valid_proof(last_block['proof'], block['proof']):
                return False

            last_block = block
            current_index += 1

        return True

    def resolve_conflicts(self) -> bool:
        """
        简易共识算法，使用网络中最长的链来作为最终链
        :return: 如果链被取代返回 True, 否则为False
        """
        neighbours = self.nodes
        new_chain = None
        max_length = len(self.chain)

        # 搜索所有的节点
        for node in neighbours:
            response = requests.get(f'http://{node}/chain')

            if response.status_code == 200:
                length = response.json()['length']
                chain = response.json()['chain']

                # 检测节点的数据可用性
                if length > max_length and self.valid_chain(chain):
                    max_length = length
                    new_chain = chain

        # 如果发现新的可用节点数据，则更新本地的链数据
        if new_chain:
            self.chain = new_chain
            return True

        return False
